/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.webdav.xml;

import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.jackrabbit.webdav.DavConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

/**
 * <code>DomUtil</code> provides some common utility methods related to w3c-DOM.
 */
public class DomUtil {

	private static Logger log = LoggerFactory.getLogger(DomUtil.class);

	/**
	 * Constant for <code>DocumentBuilderFactory</code> which is used widely to
	 * create new <code>Document</code>s
	 */
	public static DocumentBuilderFactory BUILDER_FACTORY = DocumentBuilderFactory
			.newInstance();
	static {
		BUILDER_FACTORY.setNamespaceAware(true);
		BUILDER_FACTORY.setIgnoringComments(true);
		BUILDER_FACTORY.setIgnoringElementContentWhitespace(true);
		BUILDER_FACTORY.setCoalescing(true);
	}

	/**
	 * Add a new child element with the given local name and namespace to the
	 * specified parent.
	 * 
	 * @param parent
	 * @param localName
	 * @param namespace
	 * @return the new element that was attached to the given parent.
	 */
	public static Element addChildElement(Element parent, String localName,
			Namespace namespace) {
		Element elem = createElement(parent.getOwnerDocument(), localName,
				namespace);
		parent.appendChild(elem);
		return elem;
	}

	/**
	 * Add a new child element with the given local name and namespace to the
	 * specified parent. The specified text is added as Text node to the created
	 * child element.
	 * 
	 * @param parent
	 * @param localName
	 * @param namespace
	 * @param text
	 * @return child element that was added to the specified parent
	 * @see Document#createElement(String)
	 * @see Document#createElementNS(String, String)
	 * @see Document#createTextNode(String)
	 * @see Node#appendChild(org.w3c.dom.Node)
	 */
	public static Element addChildElement(Element parent, String localName,
			Namespace namespace, String text) {
		Element elem = createElement(parent.getOwnerDocument(), localName,
				namespace, text);
		parent.appendChild(elem);
		return elem;
	}

	/**
	 * Add a new child element with the given local name and namespace to the
	 * specified parent.
	 * 
	 * @param parent
	 * @param localName
	 * @param namespace
	 * @return the new element that was attached to the given parent.
	 */
	public static Element addChildElement(Node parent, String localName,
			Namespace namespace) {
		Document doc = parent.getOwnerDocument();
		if (parent instanceof Document) {
			doc = (Document) parent;
		}
		Element elem = createElement(doc, localName, namespace);
		parent.appendChild(elem);
		return elem;
	}

	// ----------------------------------------------------< factory methods
	// >---
	/**
	 * Create a new DOM element with the specified local name and namespace.
	 * 
	 * @param factory
	 * @param localName
	 * @param namespace
	 * @return a new DOM element
	 * @see Document#createElement(String)
	 * @see Document#createElementNS(String, String)
	 */
	public static Element createElement(Document factory, String localName,
			Namespace namespace) {
		if (namespace != null) {
			return factory.createElementNS(namespace.getURI(), getPrefixedName(
					localName, namespace));
		} else {
			return factory.createElement(localName);
		}
	}

	/**
	 * Create a new DOM element with the specified local name and namespace and
	 * add the specified text as Text node to it.
	 * 
	 * @param factory
	 * @param localName
	 * @param namespace
	 * @param text
	 * @return a new DOM element
	 * @see Document#createElement(String)
	 * @see Document#createElementNS(String, String)
	 * @see Document#createTextNode(String)
	 * @see Node#appendChild(org.w3c.dom.Node)
	 */
	public static Element createElement(Document factory, String localName,
			Namespace namespace, String text) {
		Element elem = createElement(factory, localName, namespace);
		setText(elem, text);
		return elem;
	}

	/**
	 * Returns the Xml representation of a boolean isDeep, where false presents
	 * a depth value of '0', true a depth value of 'infinity'.
	 * 
	 * @param isDeep
	 * @return Xml representation
	 */
	public static Element depthToXml(boolean isDeep, Document factory) {
		return depthToXml(isDeep ? "infinity" : "0", factory);
	}

	/**
	 * Returns the Xml representation of a depth String. Webdav defines the
	 * following valid values for depths: 0, 1, infinity
	 * 
	 * @param depth
	 * @return 'deep' XML element
	 */
	public static Element depthToXml(String depth, Document factory) {
		Element dElem = createElement(factory, DavConstants.XML_DEPTH,
				DavConstants.NAMESPACE, depth);
		return dElem;
	}

	/**
	 * Returns the value of the named attribute of the current element.
	 * 
	 * @param parent
	 * @param localName
	 *            attribute local name or 'nodeName' if no namespace is
	 *            specified.
	 * @param namespace
	 *            or <code>null</code>
	 * @return attribute value, or <code>null</code> if not found
	 */
	public static String getAttribute(Element parent, String localName,
			Namespace namespace) {
		if (parent == null) {
			return null;
		}
		Attr attribute;
		if (namespace == null) {
			attribute = parent.getAttributeNode(localName);
		} else {
			attribute = parent
					.getAttributeNodeNS(namespace.getURI(), localName);
		}
		if (attribute != null) {
			return attribute.getValue();
		} else {
			return null;
		}
	}

	/**
	 * Returns the first child element that matches the given local name and
	 * namespace. If no child element is present or no child element matches,
	 * <code>null</code> is returned.
	 * 
	 * @param parent
	 * @param childLocalName
	 * @param childNamespace
	 * @return first child element matching the specified names or
	 *         <code>null</code>.
	 */
	public static Element getChildElement(Node parent, String childLocalName,
			Namespace childNamespace) {
		if (parent != null) {
			NodeList children = parent.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				Node child = children.item(i);
				if (isElement(child)
						&& matches(child, childLocalName, childNamespace)) {
					return (Element) child;
				}
			}
		}
		return null;
	}

	/**
	 * Return an <code>ElementIterator</code> over all child elements.
	 * 
	 * @param parent
	 * @return
	 * @see #getChildren(Element, String, Namespace) for a method that only
	 *      retrieves child elements that match a specific local name and
	 *      namespace.
	 */
	public static ElementIterator getChildren(Element parent) {
		return new ElementIterator(parent);
	}

	/**
	 * Returns a <code>ElementIterator</code> containing all child elements of
	 * the given parent node that match the given local name and namespace. If
	 * the namespace is <code>null</code> only the localName is compared.
	 * 
	 * @param parent
	 *            the node the children elements should be retrieved from
	 * @param childLocalName
	 * @param childNamespace
	 * @return an <code>ElementIterator</code> giving access to all child
	 *         elements that match the specified localName and namespace.
	 */
	public static ElementIterator getChildren(Element parent,
			String childLocalName, Namespace childNamespace) {
		return new ElementIterator(parent, childLocalName, childNamespace);
	}

	/**
	 * Calls {@link #getText(Element)} on the first child element that matches
	 * the given local name and namespace.
	 * 
	 * @param parent
	 * @param childLocalName
	 * @param childNamespace
	 * @return text contained in the first child that matches the given local
	 *         name and namespace or <code>null</code>.
	 * @see #getText(Element)
	 */
	public static String getChildText(Element parent, String childLocalName,
			Namespace childNamespace) {
		Element child = getChildElement(parent, childLocalName, childNamespace);
		return (child == null) ? null : getText(child);
	}

	/**
	 * Calls {@link #getTextTrim(Element)} on the first child element that
	 * matches the given local name and namespace.
	 * 
	 * @param parent
	 * @param childLocalName
	 * @param childNamespace
	 * @return text contained in the first child that matches the given local
	 *         name and namespace or <code>null</code>. Note, that leading and
	 *         trailing whitespace is removed from the text.
	 * @see #getTextTrim(Element)
	 */
	public static String getChildTextTrim(Element parent,
			String childLocalName, Namespace childNamespace) {
		Element child = getChildElement(parent, childLocalName, childNamespace);
		return (child == null) ? null : getTextTrim(child);
	}

	/**
	 * Return a list of all child nodes that are either Element, Text or CDATA.
	 * 
	 * @param parent
	 * @return a list of all child nodes that are either Element, Text or CDATA.
	 */
	public static List<Node> getContent(Node parent) {
		List<Node> content = new ArrayList<Node>();
		if (parent != null) {
			NodeList children = parent.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				Node child = children.item(i);
				if (isAcceptedNode(child)) {
					content.add(child);
				}
			}
		}
		return content;
	}

	/**
	 * Returns a string representation of the name of a DOM node consisting of
	 * "{" + namespace uri + "}" + localName. If the specified namespace is
	 * <code>null</code> or represents the empty namespace, the local name is
	 * returned.
	 * 
	 * @param localName
	 * @param namespace
	 * @return String representation of the name of a DOM node consisting of "{"
	 *         + namespace uri + "}" + localName. If the specified namespace is
	 *         <code>null</code> or represents the empty namespace, the local
	 *         name is returned.
	 * @since 2.0 Replaces the deprecated method
	 *        {@link #getQualifiedName(String, Namespace)}.
	 */
	public static String getExpandedName(String localName, Namespace namespace) {
		if (namespace == null || namespace.equals(Namespace.EMPTY_NAMESPACE)) {
			return localName;
		}
		StringBuffer b = new StringBuffer("{");
		b.append(namespace.getURI()).append("}");
		b.append(localName);
		return b.toString();
	}

	/**
	 * Return the first child element
	 * 
	 * @return the first child element or <code>null</code> if the given node
	 *         has no child elements.
	 */
	public static Element getFirstChildElement(Node parent) {
		if (parent != null) {
			NodeList children = parent.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				Node child = children.item(i);
				if (isElement(child)) {
					return (Element) child;
				}
			}
		}
		return null;
	}

	/**
	 * Build a Namespace from the prefix and uri retrieved from the given
	 * element.
	 * 
	 * @return the <code>Namespace</code> of the given element.
	 */
	public static Namespace getNamespace(Element element) {
		String uri = element.getNamespaceURI();
		String prefix = element.getPrefix();
		if (uri == null) {
			return Namespace.EMPTY_NAMESPACE;
		} else {
			return Namespace.getNamespace(prefix, uri);
		}
	}

	/**
	 * Returns the namespace attributes of the given element.
	 * 
	 * @param element
	 * @return the namespace attributes.
	 */
	public static Attr[] getNamespaceAttributes(Element element) {
		NamedNodeMap attributes = element.getAttributes();
		List<Attr> nsAttr = new ArrayList<Attr>();
		for (int i = 0; i < attributes.getLength(); i++) {
			Attr attr = (Attr) attributes.item(i);
			if (Namespace.XMLNS_NAMESPACE.getURI().equals(
					attr.getNamespaceURI())) {
				nsAttr.add(attr);
			}
		}
		return nsAttr.toArray(new Attr[nsAttr.size()]);
	}

	/**
	 * Return the qualified name of a DOM node consisting of namespace prefix +
	 * ":" + local name. If the specified namespace is <code>null</code> or
	 * contains an empty prefix, the local name is returned.<br>
	 * NOTE, that this is the value to be used for the 'qualified Name'
	 * parameter expected with the namespace sensitive factory methods.
	 * 
	 * @param localName
	 * @param namespace
	 * @return qualified name consisting of prefix, ':' and local name.
	 * @see Document#createAttributeNS(String, String)
	 * @see Document#createElementNS(String, String)
	 */
	public static String getPrefixedName(String localName, Namespace namespace) {
		if (namespace == null
				|| Namespace.EMPTY_NAMESPACE.equals(namespace)
				|| Namespace.EMPTY_NAMESPACE.getPrefix().equals(
						namespace.getPrefix())) {
			return localName;
		}
		StringBuffer buf = new StringBuffer(namespace.getPrefix());
		buf.append(":");
		buf.append(localName);
		return buf.toString();
	}

	/**
	 * Same as {@link #getExpandedName(String, Namespace)}.
	 * 
	 * @param localName
	 * @param namespace
	 * @return the expanded name of a DOM node consisting of "{" + namespace uri
	 *         + "}" + localName. If the specified namespace is
	 *         <code>null</code> or represents the empty namespace, the local
	 *         name is returned.
	 * @deprecated As of 2.0. Please use
	 *             {@link #getExpandedName(String, Namespace)} instead. This
	 *             method was named according to usage of 'qualified name' in
	 *             JSR 170 that conflicts with the terminology used in XMLNS. As
	 *             of JCR 2.0 the String consisting of
	 *             <code>"{" + namespace uri + "}" + localName</code> is called
	 *             <code>Expanded Name</code>.
	 * 
	 */
	public static String getQualifiedName(String localName, Namespace namespace) {
		return getExpandedName(localName, namespace);
	}

	/**
	 * Concatenates the values of all child nodes of type 'Text' or 'CDATA'/
	 * 
	 * @param element
	 * @return String representing the value of all Text and CDATA child nodes
	 *         or <code>null</code> if the length of the resulting String is 0.
	 * @see #isText(Node)
	 */
	public static String getText(Element element) {
		StringBuffer content = new StringBuffer();
		if (element != null) {
			NodeList nodes = element.getChildNodes();
			for (int i = 0; i < nodes.getLength(); i++) {
				Node child = nodes.item(i);
				if (isText(child)) {
					// cast to super class that contains Text and CData
					content.append(((CharacterData) child).getData());
				}
			}
		}
		return (content.length() == 0) ? null : content.toString();
	}

	/**
	 * Same as {@link #getText(Element)} except that 'defaultValue' is returned
	 * instead of <code>null</code>, if the element does not contain any text.
	 * 
	 * @param element
	 * @param defaultValue
	 * @return the text contained in the specified element or
	 *         <code>defaultValue</code> if the element does not contain any
	 *         text.
	 */
	public static String getText(Element element, String defaultValue) {
		String txt = getText(element);
		return (txt == null) ? defaultValue : txt;
	}

	/**
	 * Removes leading and trailing whitespace after calling
	 * {@link #getText(Element)}.
	 * 
	 * @param element
	 * @return Trimmed text or <code>null</code>
	 */
	public static String getTextTrim(Element element) {
		String txt = getText(element);
		return (txt == null) ? txt : txt.trim();
	}

	/**
	 * Returns true if the given parent node has a child element that matches
	 * the specified local name and namespace.
	 * 
	 * @param parent
	 * @param childLocalName
	 * @param childNamespace
	 * @return returns true if a child element exists that matches the specified
	 *         local name and namespace.
	 */
	public static boolean hasChildElement(Node parent, String childLocalName,
			Namespace childNamespace) {
		return getChildElement(parent, childLocalName, childNamespace) != null;
	}

	/**
	 * Return <code>true</code> if the given parent contains any child that is
	 * either an Element, Text or CDATA.
	 * 
	 * @param parent
	 * @return <code>true</code> if the given parent contains any child that is
	 *         either an Element, Text or CDATA.
	 */
	public static boolean hasContent(Node parent) {
		if (parent != null) {
			NodeList children = parent.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				Node child = children.item(i);
				if (isAcceptedNode(child)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Builds a 'DAV:href' Xml element from the given href. Please note, that
	 * the path present in the given String should be properly
	 * {@link org.apache.jackrabbit.util.Text#escapePath(String) escaped} in
	 * order to prevent problems with WebDAV clients.
	 * 
	 * @param href
	 *            String representing the text of the 'href' Xml element
	 * @param factory
	 *            the Document used as factory
	 * @return Xml representation of a 'href' according to RFC 2518.
	 */
	public static Element hrefToXml(String href, Document factory) {
		return createElement(factory, DavConstants.XML_HREF,
				DavConstants.NAMESPACE, href);
	}

	/**
	 * @param node
	 * @return true if the specified node is either an element or Text or CDATA
	 */
	private static boolean isAcceptedNode(Node node) {
		return isElement(node) || isText(node);
	}

	/**
	 * @param node
	 * @return true if the given node is of type element.
	 */
	static boolean isElement(Node node) {
		return node.getNodeType() == Node.ELEMENT_NODE;
	}

	/**
	 * @param node
	 * @return true if the given node is of type text or CDATA.
	 */
	static boolean isText(Node node) {
		int ntype = node.getNodeType();
		return ntype == Node.TEXT_NODE || ntype == Node.CDATA_SECTION_NODE;
	}

	/**
	 * Returns true if the specified node matches the required names. Note, that
	 * that tests return true if the required name is <code>null</code>.
	 * 
	 * @param node
	 * @param requiredLocalName
	 * @param requiredNamespace
	 * @return true if local name and namespace match the corresponding
	 *         properties of the given DOM node.
	 */
	public static boolean matches(Node node, String requiredLocalName,
			Namespace requiredNamespace) {
		if (node == null) {
			return false;
		}
		boolean matchingNamespace = matchingNamespace(node, requiredNamespace);
		return matchingNamespace && matchingLocalName(node, requiredLocalName);
	}

	/**
	 * @param node
	 * @param requiredLocalName
	 * @return true if the required local name is <code>null</code> or if the
	 *         nodes local name matches.
	 */
	private static boolean matchingLocalName(Node node, String requiredLocalName) {
		if (requiredLocalName == null) {
			return true;
		} else {
			String localName = node.getLocalName();
			return requiredLocalName.equals(localName);
		}
	}

	/**
	 * @param node
	 * @param requiredNamespace
	 * @return true if the required namespace is <code>null</code> or matches
	 *         the namespace of the specified node.
	 */
	private static boolean matchingNamespace(Node node,
			Namespace requiredNamespace) {
		if (requiredNamespace == null) {
			return true;
		} else {
			return requiredNamespace.isSame(node.getNamespaceURI());
		}
	}

	/**
	 * Add an attribute node to the given element.
	 * 
	 * @param element
	 * @param attrLocalName
	 * @param attrNamespace
	 * @param attrValue
	 */
	public static void setAttribute(Element element, String attrLocalName,
			Namespace attrNamespace, String attrValue) {
		if (attrNamespace == null) {
			Attr attr = element.getOwnerDocument().createAttribute(
					attrLocalName);
			attr.setValue(attrValue);
			element.setAttributeNode(attr);
		} else {
			Attr attr = element.getOwnerDocument().createAttributeNS(
					attrNamespace.getURI(),
					getPrefixedName(attrLocalName, attrNamespace));
			attr.setValue(attrValue);
			element.setAttributeNodeNS(attr);
		}
	}

	/**
	 * Adds a namespace attribute on the given element.
	 * 
	 * @param element
	 * @param prefix
	 * @param uri
	 */
	public static void setNamespaceAttribute(Element element, String prefix,
			String uri) {
		if (Namespace.EMPTY_NAMESPACE.equals(Namespace
				.getNamespace(prefix, uri))) {
			/**
			 * don't try to set the empty namespace which will fail see
			 * {@link org.w3c.dom.DOMException#NAMESPACE_ERR} TODO: correct?
			 */
			log.debug("Empty namespace -> omit attribute setting.");
			return;
		}
		setAttribute(element, prefix, Namespace.XMLNS_NAMESPACE, uri);
	}

	/**
	 * Create a new text node and add it as child to the given element.
	 * 
	 * @param element
	 * @param text
	 */
	public static void setText(Element element, String text) {
		if (text == null || "".equals(text)) {
			// ignore null/empty string text
			return;
		}
		Text txt = element.getOwnerDocument().createTextNode(text);
		element.appendChild(txt);
	}

	/**
	 * Converts the given timeout (long value defining the number of milli-
	 * second until timeout is reached) to its Xml representation as defined by
	 * RTF 2518.<br>
	 * Note, that {@link DavConstants#INFINITE_TIMEOUT} is not represented by
	 * the String {@link DavConstants#TIMEOUT_INFINITE 'Infinite'} defined by
	 * RFC 2518, due to a known issue with Microsoft Office that opens the
	 * document "read only" and never unlocks the resource if the timeout is
	 * missing or 'Infinite'.
	 * 
	 * @param timeout
	 *            number of milli-seconds until timeout is reached.
	 * @return 'timeout' Xml element
	 */
	public static Element timeoutToXml(long timeout, Document factory) {
		String expString = "Second-" + timeout / 1000;
		Element exp = createElement(factory, DavConstants.XML_TIMEOUT,
				DavConstants.NAMESPACE, expString);
		return exp;
	}
}